/**
 * 自定义节点重叠
 * @author zhouyuhua
 * 算法总体思路：
 * 1、为每个节点创建一个新的属性layoutData，
 *    (a)、neighbours -> 邻接节点列表
 *    (b)、dx -> x轴需要移动的距离
 *    (c)、dy -> y轴需要移动的距离
 * 2、根据所有节点的位置，获取节点所在画布的x，y轴的最大最小值
 * 3、创建一个空间网格，将整个画布分割成20*20的网格
 * 4、将所有节点加入到网格中，根据节点的坐标和半径，确定这个节点占据了网格中哪些位置
 * 5、构建每个节点的邻接表，将相邻的节点或者重合的节点加入邻接表
 * 6、对每个节点与其邻接节点的冲突进行判断，若产生冲突，则在其dx，dy修改
 * 7、对每个节点对其中心进行dx和dy偏移
 * 8、循环以上操作，直至无节点重叠
 */
// eslint-disable-next-line max-classes-per-file
import G6 from '@antv/g6'
import { message } from 'ant-design-vue'

const ratio = 1 // 缩放比例
const margin = 5 // 间距
const speed = 3 // 速度

// 每个Cell
class Cell {
  public row: number = 0
  public col: number = 0

  constructor(row: number, col: number) {
    this.row = row
    this.col = col
  }

  public toString() {
    return `${this.row},${this.col}`
  }
}

// 构建一个空间grid
class SpatialGrid {
  // 参数
  private COLNUMS_ROWS = 20
  // 数据
  public data = new Map()
  // 所有节点的最大最小值
  private xmin = 0
  private xmax = 0
  private ymin = 0
  private ymax = 0

  constructor(xmin: number, xmax: number, ymin: number, ymax: number) {
    this.xmin = xmin
    this.xmax = xmax
    this.ymin = ymin
    this.ymax = ymax

    for (let row = 0; row < this.COLNUMS_ROWS; row += 1) {
      for (let col = 0; col < this.COLNUMS_ROWS; col += 1) {
        const localNodes: any[] = []
        this.data.set(new Cell(row, col).toString(), localNodes)
      }
    }
  }

  // 获得在row行col列的数据
  public getContent(row: number, col: number) {
    return this.data.get(new Cell(row, col).toString())
  }

  // 获得行数
  public countRows() {
    return this.COLNUMS_ROWS
  }

  // 获得列数
  public countColumns() {
    return this.COLNUMS_ROWS
  }

  // 添加节点
  public add(node: any) {
    const { x, y } = node
    const radius = node.size

    // 获取节点矩形位置
    const nxmin = x - (radius * ratio + margin)
    const nxmax = x + (radius * ratio + margin)
    const nymin = y - (radius * ratio + margin)
    const nymax = y + (radius * ratio + margin)

    // 获取矩形作为盒子
    const minXbox = Math.floor(
      ((this.COLNUMS_ROWS - 1) * (nxmin - this.xmin)) / (this.xmax - this.xmin)
    )
    const maxXbox = Math.floor(
      ((this.COLNUMS_ROWS - 1) * (nxmax - this.xmin)) / (this.xmax - this.xmin)
    )
    const minYbox = Math.floor(
      ((this.COLNUMS_ROWS - 1) * (nymin - this.ymin)) / (this.ymax - this.ymin)
    )
    const maxYbox = Math.floor(
      ((this.COLNUMS_ROWS - 1) * (nymax - this.ymin)) / (this.ymax - this.ymin)
    )

    // 将x，y被这个节点覆盖了
    for (let col = minXbox; col <= maxXbox; col += 1) {
      for (let row = minYbox; row <= maxYbox; row += 1) {
        try {
          this.data.get(new Cell(row, col).toString()).push(node)
        } catch {
          console.log('error')
        }
      }
    }
  }
}

/**
 * 自定义画布布局
 * 算法流程：
 */
export default function registerNoverlapLayout() {
  // 基础弧线布局
  G6.registerLayout('noverlap', {
    // 定义自定义行为的默认参数，会与用户传入的参数进行合并
    getDefaultCfg() {
      return {}
    },
    /**
     * 初始化
     * @param {Object} data 数据
     */
    init(data: any) {
      const currentLayout = this // this 指向当前布局
      currentLayout.nodes = data.nodes
      currentLayout.edges = data.edges
    },
    /**
     * 执行布局
     */
    execute() {
      const currentLayout = this
      const { nodes } = currentLayout

      let collision = true

      // 循环判断是否有节点产生冲突
      while (collision) {
        collision = currentLayout.executeOnce(nodes)
      }
    },

    // 执行一次
    executeOnce(nodes: any) {
      let isCollision = false

      try {
        // 重置布局数据
        nodes.forEach((node: any) => {
          node.layoutData = {
            neighbours: [],
            dx: 0,
            dy: 0,
          }
        })

        // 获得所有节点的最大最小值
        let xmin = 10000000
        let xmax = -1000000
        let ymin = 10000000
        let ymax = -1000000

        nodes.forEach((node: any) => {
          const { x, y, size } = node

          // 获取节点覆盖的矩形
          const nxmin = x - (size * ratio + margin)
          const nxmax = x + (size * ratio + margin)
          const nymin = y - (size * ratio + margin)
          const nymax = y + (size * ratio + margin)

          // 更新最大最小值
          xmin = Math.min(xmin, nxmin)
          xmax = Math.max(xmax, nxmax)
          ymin = Math.min(ymin, nymin)
          ymax = Math.max(ymax, nymax)
        })

        // 获取图的长宽和中心
        const xwidth = xmax - xmin
        const yheight = ymax - ymin
        const xcenter = (xmin + xmax) / 2
        const ycenter = (ymin + ymax) / 2

        // 将整个图长宽扩展为原来的securityRatio倍数
        const securityRatio = 1.1
        xmin = xcenter - (securityRatio * xwidth) / 2
        xmax = xcenter + (securityRatio * xwidth) / 2
        ymin = ycenter - (securityRatio * yheight) / 2
        ymax = ycenter + (securityRatio * yheight) / 2

        // 把所有节点放进一个空间的grid里面
        const grid = new SpatialGrid(xmin, xmax, ymin, ymax)
        nodes.forEach((node: any) => {
          grid.add(node)
        })

        // 构建一个邻接表，存储与该节点冲突覆盖的节点列表
        for (let row = 0; row < grid.countRows(); row += 1) {
          for (let col = 0; col < grid.countColumns(); col += 1) {
            grid.getContent(row, col).forEach((node: any) => {
              const { layoutData } = node

              for (
                let row2 = Math.max(0, row - 1);
                row2 <= Math.min(row + 1, grid.countRows());
                row2 += 1
              ) {
                for (
                  let col2 = Math.max(0, col - 1);
                  col2 <= Math.min(col + 1, grid.countColumns());
                  col2 += 1
                ) {
                  grid.getContent(row2, col2).forEach((node2: any) => {
                    if (
                      node.id !== node2.id &&
                      layoutData.neighbours.findIndex(
                        (n: any) => n.id === node2.id
                      ) < 0
                    ) {
                      layoutData.neighbours.push(node2)
                    }
                  })
                }
              }
            })
          }
        }

        // 计算每个节点需要移动的距离
        nodes.forEach((node: any) => {
          const { layoutData } = node

          // 遍历每个冲突的节点和邻近节点
          layoutData.neighbours.forEach((node2: any) => {
            const n1x = node.x
            const n1y = node.y
            const n2x = node2.x
            const n2y = node2.y
            const n1radius = node.size
            const n2radius = node2.size

            const xDist = n2x - n1x
            const yDist = n2y - n1y

            // 计算两个节点的距离
            const dist = Math.sqrt(xDist * xDist + yDist * yDist)
            // 如果两个节点的距离小于圆心 + 半径 + marin，则为冲突
            const collision =
              dist < n1radius * ratio + margin + (n2radius * ratio + margin)

            // 修改全局冲突，如果没有节点冲突，那么布局结束
            isCollision = isCollision || collision

            // 如果产生冲突
            if (collision) {
              const laid = node2.layoutData
              const f = 1 + node.size

              if (dist > 0) {
                // 距离大于0，部分重合
                laid.dx += (xDist / dist) * f
                laid.dy += (yDist / dist) * f
              } else {
                // 距离小于0，完全重合
                laid.dx += 0.01 * (0.5 - Math.random())
                laid.dy += 0.01 * (0.5 - Math.random())
              }
            }
          })
        })

        // 修改有重叠的节点的位置
        nodes.forEach((node: any) => {
          const { layoutData } = node
          layoutData.dx *= 0.1 * speed
          layoutData.dy *= 0.1 * speed
          node.x += layoutData.dx
          node.y += layoutData.dy

          // 删除邻接属性
          delete node.layoutData
        })
      } catch {
        message.error('布局失败！')
        return false
      }

      return isCollision
    },

    /**
     * 根据传入的数据进行布局
     * @param {Object} data 数据
     */
    layout(data: any) {
      const currentLayout = this
      currentLayout.init(data)
      currentLayout.execute()
    },
    /**
     * 更新布局配置，但不执行布局
     * @param {Object} cfg 需要更新的配置项
     */
    updateCfg(cfg: any) {
      const currentLayout = this
      G6.Util.mix(currentLayout, cfg)
    },
    /**
     * 销毁
     */
    destroy() {
      const currentLayout = this
      currentLayout.positions = null
      currentLayout.nodes = null
      currentLayout.edges = null
      currentLayout.destroyed = true
    },
  })
}
